package com.gitee.dolt.log.aspect;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.convert.ConvertException;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.json.JSONUtil;
import com.gitee.dolt.annotation.log.SysLog;
import com.gitee.dolt.core.base.Result;
import com.gitee.dolt.core.context.ContextConstants;
import com.gitee.dolt.core.context.ContextUtil;
import com.gitee.dolt.core.exception.code.ExceptionCode;
import com.gitee.dolt.core.util.SpelUtils;
import com.gitee.dolt.core.util.SpringUtils;
import com.gitee.dolt.enumeration.OptLogOut;
import com.gitee.dolt.model.OptLogDTO;
import com.gitee.dolt.event.OptLogEvent;
import com.gitee.dolt.utils.NumberUtils;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Optional;

/**
 * 日志切面
 * 操作日志使用spring event异步入库
 * <p>
 *
 * @author LYH
 */
@Slf4j
@Aspect
public class SysLogAspect {

    private static final ThreadLocal<OptLogDTO> THREAD_LOCAL = new ThreadLocal<>();


    /***
     * 定义controller切入点拦截规则：拦截标记SysLog注解和指定包下的方法
     * 标记了@SysLog 注解的方法
     */
    @Pointcut("@annotation(com.gitee.dolt.annotation.log.SysLog) " +
            "|| @within(com.gitee.dolt.annotation.log.SysLog)")
    public void sysLogAspect() {
    }

    @Before(value = "sysLogAspect()")
    public void doBefore(JoinPoint joinPoint) {
        SysLog sysLogAnnotation = getTargetAnnotation(joinPoint);
        if (!check(sysLogAnnotation)) {
            return;
        }

        // 开始时间
        OptLogDTO sysLog = get();
        sysLog.setUserId(ContextUtil.getUserIdStr());
        String controllerDescription = "";
        Api api = joinPoint.getTarget().getClass().getAnnotation(Api.class);
        if (api != null) {
            String[] tags = api.tags();
            if (tags != null && tags.length > 0) {
                controllerDescription = tags[0];
            }
        }

        String controllerMethodDescription = sysLogAnnotation == null ? StrUtil.EMPTY : sysLogAnnotation.value();

        if (StrUtil.isNotEmpty(controllerMethodDescription)) {
            //获取方法参数值
            Object[] args = joinPoint.getArgs();

            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            try {
                controllerMethodDescription = SpelUtils.getValBySpEL(controllerMethodDescription, methodSignature.getMethod(), args);
            } catch (SpelEvaluationException e) {
                log.warn(e.getMessage(), e);
            }

        }

        if (StrUtil.isEmpty(controllerDescription)) {
            sysLog.setOptDesc(controllerMethodDescription);
        } else {
            sysLog.setOptDesc(controllerDescription + "-" + controllerMethodDescription);
        }

        String optModule = sysLogAnnotation.optModule();
        if (StrUtil.isEmpty(optModule)) {
            optModule = Optional.ofNullable(getClassAnnotation(joinPoint)).map(SysLog::optModule).orElse(StrUtil.EMPTY);
        }
        sysLog.setOptModule(optModule);
        sysLog.setOptType(sysLogAnnotation.optType());

        // 类名
        sysLog.setClassPath(joinPoint.getTarget().getClass().getName());
        //获取执行的方法名
        sysLog.setActionMethod(joinPoint.getSignature().getName());

        // 参数
        Object[] args = joinPoint.getArgs();
        HttpServletRequest request = getRequest();
        if (request != null) {
            String strArgs = getArgs(args, request);
            sysLog.setParams(getText(strArgs));
            sysLog.setRequestIp(ServletUtil.getClientIP(request));
            sysLog.setRequestUri(URLUtil.getPath(request.getRequestURI()));
            sysLog.setHttpMethod(request.getMethod());
            sysLog.setUa(StrUtil.sub(request.getHeader("user-agent"), 0, 500));
            sysLog.setTrace(request.getHeader(ContextConstants.TRACE_ID_HEADER));
        }

        sysLog.setStartTime(LocalDateTime.now());

        THREAD_LOCAL.set(sysLog);
    }

    /**
     * 返回通知
     *
     * @param ret
     * @throws Throwable
     */
    @AfterReturning(returning = "ret", pointcut = "sysLogAspect()")
    public void doAfterReturning(JoinPoint joinPoint, Object ret) {
        SysLog sysLogAnnotation = getTargetAnnotation(joinPoint);
        if (!check(sysLogAnnotation)) {
            return;
        }

        Result r = null;

        try {
            r = Convert.convert(Result.class, ret);
        } catch (ConvertException e) {
        }

        OptLogDTO sysLog = get();
        if (r == null) {
            sysLog.setType("OPT");
            if (sysLogAnnotation.response()) {
                sysLog.setResult(getText(String.valueOf(ret == null ? StrUtil.EMPTY : ret)));
            }
        } else {
            if (r.getCode() == ExceptionCode.OK.getCode()) {
                sysLog.setType("OPT");
            } else {
                sysLog.setType("EX");
                sysLog.setExDetail(r.getMessage());
            }
            if (sysLogAnnotation.response()) {
                sysLog.setResult(getText(r.toString()));
            }
        }

        OptLogOut out = getAndCheckOut(sysLogAnnotation, joinPoint);
        publishEvent(sysLog, out);

    }


    /**
     * 异常通知
     *
     * @param e
     */
    @AfterThrowing(pointcut = "sysLogAspect()", throwing = "e")
    public void doAfterThrowable(JoinPoint joinPoint, Throwable e) {
        SysLog sysLogAnnotation = getTargetAnnotation(joinPoint);
        if (!check(sysLogAnnotation)) {
            return;
        }

        OptLogDTO sysLog = get();
        sysLog.setType("EX");

        // 遇到错误时，请求参数若为空，则记录
        Object[] args = joinPoint.getArgs();

        HttpServletRequest request = getRequest();
        if (request != null) {
            String strArgs = getArgs(args, request);
            sysLog.setParams(getText(strArgs));
        }

        // 异常对象
        sysLog.setExDetail(ExceptionUtil.stacktraceToString(e, NumberUtils.NUM_65535));
        // 异常信息
        sysLog.setExDesc(ExceptionUtil.stacktraceToString(e, NumberUtils.NUM_65535));


        OptLogOut out = getAndCheckOut(sysLogAnnotation, joinPoint);
        publishEvent(sysLog, out);
    }

    private OptLogOut getAndCheckOut(SysLog sysLogAnnotation, JoinPoint joinPoint) {
        OptLogOut out = sysLogAnnotation.out();
        if (OptLogOut.DEFAULT.equals(out)) {
            out = Optional.ofNullable(getClassAnnotation(joinPoint)).map(SysLog::out).orElse(OptLogOut.DEFAULT);
        }
        return out;
    }

    /**
     * 获取@SysLog
     * 首先从方法上获取，获取不到再从类上获取
     *
     * @param joinPoint
     * @return
     */
    private SysLog getTargetAnnotation(JoinPoint joinPoint) {
        return Optional.ofNullable(getMethodAnnotation(joinPoint)).orElse(getClassAnnotation(joinPoint));
    }

    /**
     * 获取方法上的注解
     *
     * @param joinPoint
     * @return
     */
    private SysLog getMethodAnnotation(JoinPoint joinPoint) {
        if (joinPoint.getSignature() instanceof MethodSignature) {
            Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
            if (method != null) {
                return method.getAnnotation(SysLog.class);
            }
        }
        return null;
    }

    /**
     * 获取类上的注解
     *
     * @param joinPoint
     * @return
     */
    private SysLog getClassAnnotation(JoinPoint joinPoint) {
        return joinPoint.getTarget().getClass().getAnnotation(SysLog.class);
    }

    private HttpServletRequest getRequest() {
        HttpServletRequest request = Optional.ofNullable(RequestContextHolder.getRequestAttributes())
                .map(r -> (ServletRequestAttributes) r).map(r -> r.getRequest()).orElse(null);
        return request;
    }

    /**
     * 监测是否需要记录日志
     *
     * @param sysLogAnnotation
     * @return
     */
    private boolean check(SysLog sysLogAnnotation) {
        return sysLogAnnotation == null || sysLogAnnotation.enabled();
    }


    private OptLogDTO get() {
        OptLogDTO sysLog = THREAD_LOCAL.get();
        if (sysLog == null) {
            return new OptLogDTO();
        }
        return sysLog;
    }


    private void publishEvent(OptLogDTO sysLog, OptLogOut out) {
        sysLog.setFinishTime(LocalDateTime.now());
        sysLog.setConsumingTime(sysLog.getStartTime().until(sysLog.getFinishTime(), ChronoUnit.MILLIS));
        SpringUtils.publishEvent(new OptLogEvent(sysLog, out));
        THREAD_LOCAL.remove();
    }

    /**
     * 截取指定长度的字符串
     *
     * @param val
     * @return
     */
    private String getText(String val) {
        return StrUtil.sub(val, 0, NumberUtils.NUM_65535);
    }

    /**
     * 获取请求参数
     *
     * @param args
     * @param request
     * @return
     */
    private String getArgs(Object[] args, HttpServletRequest request) {
        String strArgs = StrUtil.EMPTY;
        try {
            if (!request.getContentType().contains("multipart/form-data")) {
                strArgs = JSONUtil.toJsonStr(args);
            }
        } catch (Exception e) {
            try {
                strArgs = Arrays.toString(args);
            } catch (Exception ex) {
                log.warn("解析参数异常", ex);
            }
        }
        return strArgs;
    }


}
